using System;
using System.IO;
using System.Runtime.CompilerServices;

namespace Pinwheel.LSerialize
{
    public unsafe partial struct LStreamBuffer : ILStreamBufferWriter, IDisposable
    {
        public const int DefaultSize = 512;
        public ILStreamBufferWriter Writer => this;
        
        private byte* head;
        private int index;
        private int length;
        private int tail;

        public bool IsValid => null != head;
        public int Size => tail;
        public int Index => index;
        public int Free => length - index;
        public bool Enough => index < length;
        public bool IsEmpty => length == 0;
        public int Tail => tail;
        
        public byte* Head
        {
            get
            {
                // if (IsEmpty) Allocate(DefaultSize);
                if (!Enough) EnsureSize(1);
                return head;
            }
        }
        
        public byte* Current => head + index;
        // {
        //     get
        //     {
        //         EnsureSize(index - length); 
        //         return head + index;
        //     }
        // }
        
        public byte this[int i]
        {
            get
            {
                if (i < 0 || i >= length)
                {
                    throw new Exception("invalid idx:" + i + "@get");
                }

                return *(head + i);
            }

            set
            {
                if (i < 0 || i >= length)
                {
                    throw new Exception("invalid idx:" + i + "@set");
                }

                *(head + i) = value;
            }
        }
        
        public static bool operator ==(LStreamBuffer left, LStreamBuffer right)
        {
            return left.head == right.head;
        }

        public static bool operator !=(LStreamBuffer left, LStreamBuffer right)
        {
            return !(left == right);
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int PushByte(byte value)
        {
            var size = sizeof(byte);
            EnsureSize(size);
            *(head + index++) = value;
            if (index > tail) tail = index;
            return size;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int PushBytes(byte[] bytes)
        {
            fixed(byte* source = bytes)
                return PushBytes(source, bytes.Length);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int PushBytes(byte* source, int size)
        {
            EnsureSize(size);
            Native.Utils.Copy(Current, source, size);
            index += size;
            if (index > tail) tail = index;
            return size;
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int Seek(int offset, SeekOrigin loc)
        {
            switch (loc)
            {
                case SeekOrigin.Begin:
                    index = offset;
                    break;
                case SeekOrigin.Current:
                    index += offset;
                    break;
                case SeekOrigin.End:
                    index = tail + offset;
                    break;
            }
            return index;
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Allocate(int size)
        {
            Profiler.BeginSample("LStreamBuffer.Allocate");
            // var cache = LSerializeExtension.Cache.GetReference(this);
            // LSerializeExtension.Cache.UnbindReference(this);
            var newBuffer = Native.Current.AllocAndClear(size);
            if (null != head)
            {
                Native.Utils.Copy(newBuffer, head, length);
                Native.Current.Free(head);
            }
            head = (byte*) newBuffer;
            length = size;
            // LSerializeExtension.Cache.BindReference(this, cache);
            Profiler.EndSample();
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void EnsureSize(int size)
        {
            if (Free >= size) return;
            int enough = length;
            int free = Free;
            while (free < size)
            {
                free += enough;
                enough <<= 1;
            }
            Allocate(enough);
        }
        
        // public LStreamBuffer(int length = DefaultSize)
        // {
        //     head = (byte*) Native.Current.Alloc(length);
        //     this.length = length;
        //     index = 0;
        //     tail = 0;
        // }
        //
        // public LStreamBuffer(byte* source, int size)
        // {
        //     head = source;
        //     length = size;
        //     index = 0;
        //     tail = 0;   
        // }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Flush() => tail = tail > index ? tail : index;
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Clear()
        {
            index = 0;
            tail = 0;
        }
        
        public static LStreamBuffer* Create(int length = DefaultSize)
        {
            LStreamBuffer* buffer = Native.Current.AllocAndClear<LStreamBuffer>();
            buffer->head = (byte*) Native.Current.AllocAndClear(length);
            buffer->length = length;
            buffer->index = 0;
            buffer->tail = 0;
            return buffer;
        }


        public override int GetHashCode()
        {
            return (int)head;
        }

        public void Dispose()
        {
            Native.Current.Free(head);
            head = null;
            length = 0;
            index = 0;
            tail = 0;
        }
    }
}
